/*
 *
 *  *    Copyright 2020-2021 luter.me
 *  *
 *  *    Licensed under the Apache License, Version 2.0 (the "License");
 *  *    you may not use this file except in compliance with the License.
 *  *    You may obtain a copy of the License at
 *  *
 *  *      http://www.apache.org/licenses/LICENSE-2.0
 *  *
 *  *    Unless required by applicable law or agreed to in writing, software
 *  *    distributed under the License is distributed on an "AS IS" BASIS,
 *  *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  *    See the License for the specific language governing permissions and
 *  *    limitations under the License.
 *
 */

package com.luter.heimdall.plugins.thymeleaf.util;

import org.slf4j.Logger;
import org.thymeleaf.context.ITemplateContext;
import org.thymeleaf.engine.AttributeName;
import org.thymeleaf.exceptions.TemplateProcessingException;
import org.thymeleaf.model.IProcessableElementTag;
import org.thymeleaf.standard.expression.IStandardExpression;
import org.thymeleaf.standard.expression.IStandardExpressionParser;
import org.thymeleaf.standard.expression.StandardExpressionParser;
import org.thymeleaf.util.EvaluationUtils;
import org.thymeleaf.util.StringUtils;

import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static java.util.Collections.unmodifiableList;
import static org.slf4j.LoggerFactory.getLogger;
import static org.thymeleaf.util.StringUtils.trim;
import static org.thymeleaf.util.Validate.notEmpty;
import static org.thymeleaf.util.Validate.notNull;

/**
 * The type Thymeleaf facade.
 *
 * @author luter
 */
public final class ThymeleafFacade {
    private static final transient Logger log = getLogger(ThymeleafFacade.class);

    /**
     * Instantiates a new Thymeleaf facade.
     */
    private ThymeleafFacade() {
        throw new UnsupportedOperationException();
    }

    /**
     * Gets raw value.
     *
     * @param element       the element
     * @param attributeName the attribute name
     * @return the raw value
     */
    public static String getRawValue(final IProcessableElementTag element, final AttributeName attributeName) {
        notNull(element, "element must not be null");
        notNull(attributeName, "attributeName must not be empty");

        final String rawValue = trim(element.getAttributeValue(attributeName));
        notEmpty(rawValue, "value of '" + attributeName + "' must not be empty");

        return rawValue;
    }

    /**
     * Gets raw value.
     *
     * @param element       the element
     * @param attributeName the attribute name
     * @return the raw value
     */
    public static String getRawValue(final IProcessableElementTag element, final String attributeName) {
        notNull(element, "element must not be null");
        notEmpty(attributeName, "attributeName must not be empty");

        final String rawValue = trim(element.getAttributeValue(attributeName));
        notEmpty(rawValue, "value of '" + attributeName + "' must not be empty");

        return rawValue;
    }

    /**
     * Evaluate expression object.
     *
     * @param arguments  the arguments
     * @param expression the expression
     * @return the object
     * @throws TemplateProcessingException the template processing exception
     */
    public static Object evaluateExpression(ITemplateContext arguments, String expression) throws TemplateProcessingException {
        notNull(arguments, "arguments must not be null");
        notEmpty(expression, "expression must not be empty");

        final IStandardExpressionParser parser = new StandardExpressionParser();

        final IStandardExpression evaluableExpression = parser.parseExpression(arguments, expression);

        return evaluableExpression.execute(arguments);
    }

    /**
     * Evaluate as iterable list.
     *
     * @param arguments the arguments
     * @param rawValue  the raw value
     * @return the list
     * @throws TemplateProcessingException the template processing exception
     */
    public static List<Object> evaluateAsIterable(ITemplateContext arguments, String rawValue) throws TemplateProcessingException {
        notNull(arguments, "arguments must not be null");
        notEmpty(rawValue, "rawValue must not be empty");

        final Object evaluatedExpression = evaluateExpression(arguments, rawValue);

        return EvaluationUtils.evaluateAsList(evaluatedExpression);
    }

    /**
     * Evaluate as iterable or raw value list.
     *
     * @param arguments the arguments
     * @param rawValue  the raw value
     * @return the list
     */
    public static List<Object> evaluateAsIterableOrRawValue(ITemplateContext arguments, String rawValue) {
        notNull(arguments, "arguments must not be null");
        notEmpty(rawValue, "rawValue must not be empty");

        final List<Object> result = new ArrayList<Object>();
        try {
            result.addAll(evaluateAsIterable(arguments, rawValue));
        } catch (TemplateProcessingException ex) {
            result.add(rawValue);
        }

        return unmodifiableList(result);
    }

    /**
     * Evaluate as strings with delimiter list.
     *
     * @param arguments the arguments
     * @param rawValue  the raw value
     * @param delimiter the delimiter
     * @return the list
     */
    public static List<String> evaluateAsStringsWithDelimiter(ITemplateContext arguments, String rawValue, String delimiter) {
        notNull(arguments, "arguments must not be null");
        notEmpty(rawValue, "rawValue must not be empty");
        notEmpty(delimiter, "delimiter must not be empty");

        final List<String> result = new ArrayList<>();
        final List<Object> iterates = evaluateAsIterableOrRawValue(arguments, rawValue);

        for (Object o : iterates) {
            result.addAll(Arrays.asList(StringUtils.split(o, delimiter)));
        }

        return unmodifiableList(result);
    }


    /**
     * Gets principal property.
     *
     * @param principal the principal
     * @param property  the property
     * @return the principal property
     */
    public static String getPlainObjectProperty(final Object principal, final String property) {
        if (null == principal) {
            String message = "Error reading property [" + property + "] from principal of type [null]";
            throw new IllegalArgumentException(message);
        }
        try {
            final BeanInfo bi = Introspector.getBeanInfo(principal.getClass());
            for (final PropertyDescriptor pd : bi.getPropertyDescriptors()) {
                if (pd.getName().equals(property)) {
                    final Object value = pd.getReadMethod().invoke(principal, (Object[]) null);
                    return String.valueOf(value);
                }
            }
        } catch (final Exception e) {
            String message = "Error reading property [" + property + "] from principal of type [" + principal.getClass().getName() + "]";
            throw new IllegalArgumentException(message, e);
        }
        String error = "Property [" + property + "] not found in principal of type [" + principal.getClass().getName() + "]";
        log.error("[getPrincipalProperty]::principal = [{}], property = [{}],error = [{}]", principal, property, error);
        return "PROPERTY ERROR: [" + property + "]";
//        throw new IllegalArgumentException("Property [" + property + "] not found in principal of type [" + principal.getClass().getName() + "]");
    }
}
